package controller;

import java.util.ArrayList;
import java.util.ListIterator;

import manager.BidMgr;
import manager.RoundMgr;
import manager.SectionStudentMgr;
import manager.StudentMgr;
import entity.Bid;
import entity.Round;
import entity.Section;

public class RoundClearingLogic {

	public static final double MIN_BID_AMOUNT = 10;
	private static final double MIN_BID_INCREMENT_AMOUNT = 1;

	private RoundMgr roundMgr;
	private BidMgr bidMgr;
	private SectionStudentMgr sectionStudentMgr;
	private StudentMgr studentMgr;
	private SectionStudentCtrl sectionStudentCtrl;

	public RoundClearingLogic() {
		roundMgr = new RoundMgr();
		bidMgr = new BidMgr();
		sectionStudentMgr = new SectionStudentMgr();
		studentMgr = new StudentMgr();
		sectionStudentCtrl = new SectionStudentCtrl();
	}

	/*
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * =========================================================================
	 * COMPUTATIONS
	 * =========================================================================
	 */

	/**
	 * Computes clearing price. Clearing price is the bid amount of the last bid
	 * just before vacancy before no more vacancy.
	 * 
	 * @param sortedBids
	 *            bids sorted descending order of bid amount
	 * @param vacancy
	 *            number of vacancy available in the section
	 * @return the clearing price
	 */
	private double computeClearingPrice(ArrayList<Bid> sortedBids, int vacancy) {
		if (!sortedBids.isEmpty()) {
			int numBids = sortedBids.size();
			if (numBids < vacancy || vacancy == 0) {
				return sortedBids.get(numBids - 1).getBidAmount();
			} else if (numBids >= vacancy) {
				return sortedBids.get(vacancy - 1).getBidAmount();
			}
		}

		return MIN_BID_AMOUNT;
	}

	private double computeMinBidAmountForSectionInRoundTwo(Section section) {

		ArrayList<Bid> sortedBids = bidMgr
				.retrieveBidsForSectionOrderedByHighestBidAmount(section
						.getCourse().getCourseCode(), section.getSectionCode());
		int numBids = sortedBids.size();
		int vacancy = sectionStudentCtrl.computeVacancyForSection(section);

		if (sortedBids.isEmpty() || numBids < vacancy) {
			double currentMinBidAmount = roundMgr
					.retrieveMinBidAmountForSection(section.getCourse()
							.getCourseCode(), section.getSectionCode());
			return Math.max(currentMinBidAmount, MIN_BID_AMOUNT);
		} else {
			double newComputedMinBidAmount = computeClearingPrice(sortedBids,
					vacancy) + MIN_BID_INCREMENT_AMOUNT;
			return Math.max(newComputedMinBidAmount, MIN_BID_AMOUNT);
		}
	}

	public void computeAndStoreMinBidAmountForSectionInRoundTwo(Section section) {
		double newMinBidAmount = computeMinBidAmountForSectionInRoundTwo(section);
		roundMgr.updateMinBidAmountForSection(newMinBidAmount, section
				.getCourse().getCourseCode(), section.getSectionCode());
	}

	/**
	 * Computes index of the last clearable bid in a particular section based on
	 * the round clearing logic. This computation works for both round one and
	 * round two.
	 * 
	 * <b>COMPUTATION CASES</b>
	 * <ol>
	 * <li>NUM OF BIDS < VACANCY. When, in ROUND #1, follow CASE 2. When in
	 * ROUND #2, all bids are clearable.
	 * <li>
	 * <li>NUM OF BIDS > VACANCY. Bids below clearing price will be dropped.
	 * Multiple bids with clearing price will be dropped. Return index of last
	 * bid that is not dropped.</li>
	 * <li>NUM OF BIDS == VACANCY. When in ROUND #1, follow CASE 2 logic. When
	 * in ROUND #2, all bids will go in, so follow CASE 1 logic.</li>
	 * <li>VACANCY == 0, no clearable bid. Return -1.</li>
	 * </ol>
	 * 
	 * @param sortedBids
	 *            bids sorted in descending order of bid amount
	 * @param vacancy
	 *            the number of vacancy available in the section
	 * @return the index of last clearable bid in the section; -1 if there are
	 *         no clearable bids
	 */
	protected int computeIndexOfLastClearableBid(ArrayList<Bid> sortedBids,
			int vacancy, int roundNo) {

		int numBids = sortedBids.size();

		if (numBids == 0 || vacancy == 0) {
			return -1;
		}

		if (roundNo == Round.ROUND_ONE || numBids > vacancy) {
			/*
			 * ALL CASES in Round 1 or CASE 2 in Round 2
			 */

			int indexOfCutOffBid = Math.min(numBids, vacancy) - 1;

			double clearingPrice = computeClearingPrice(sortedBids, vacancy);

			/*
			 * If have multiple bids at clearing price, and in round 2 the next
			 * bid to cut of bid also is also placed at clearing price.
			 */
			if (hasMultipleBidsAtClearingPrice(sortedBids, clearingPrice,
					indexOfCutOffBid)) {
				if (roundNo == Round.ROUND_TWO
						&& !isNextBidPlacedAtClearingPrice(sortedBids,
								clearingPrice, indexOfCutOffBid)) {
					return indexOfCutOffBid;
				}

				return computeFirstIndexOfBidAtClearingPrice(sortedBids,
						clearingPrice, indexOfCutOffBid) - 1;
			} else {
				return indexOfCutOffBid;
			}
		} else if (numBids <= vacancy) {

			/*
			 * CASE 1 Round #2 or CASE 3 Round #2
			 */

			return numBids - 1;

		}

		/*
		 * CASE 4
		 */

		return -1;

	}

	private int computeFirstIndexOfBidAtClearingPrice(
			ArrayList<Bid> sortedBids, double clearingPrice,
			int indexOfCutOffBid) {

		int firstIndexOfBidAtClearingPrice = indexOfCutOffBid;

		ListIterator<Bid> i = sortedBids.listIterator(indexOfCutOffBid);
		while (i.hasPrevious() && i.previous().getBidAmount() == clearingPrice) {
			firstIndexOfBidAtClearingPrice--;
		}

		return firstIndexOfBidAtClearingPrice;
	}

	/*
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * =========================================================================
	 * PROCESS BIDS
	 * =========================================================================
	 */

	/**
	 * Obtains a new array list of successful bids based on the given index of
	 * last clearable bid.
	 * 
	 * @param sortedBids
	 * @param indexOfLastClearableBid
	 * @return a new array list of successful bids
	 */
	private ArrayList<Bid> findSuccessfulBids(ArrayList<Bid> sortedBids,
			int indexOfLastClearableBid) {

		ArrayList<Bid> succesfulBids = new ArrayList<Bid>();

		if (!sortedBids.isEmpty() && indexOfLastClearableBid > -1) {

			succesfulBids = new ArrayList<Bid>(sortedBids.subList(0,
					indexOfLastClearableBid + 1));
		}

		return succesfulBids;
	}

	/**
	 * Obtains a new array list of unsuccessful bids based on the given index of
	 * last clearable bid.
	 * 
	 * @param sortedBids
	 * @param indexOfLastClearableBid
	 * @return a new array list of unsuccessful bids
	 */
	private ArrayList<Bid> findUnsuccessfulBids(ArrayList<Bid> sortedBids,
			int indexOfLastClearableBid) {
		return new ArrayList<Bid>(sortedBids.subList(
				indexOfLastClearableBid + 1, sortedBids.size()));
	}

	private boolean isNextBidPlacedAtClearingPrice(ArrayList<Bid> sortedBids,
			double clearingPrice, int indexOfBid) {
		try {
			return sortedBids.get(indexOfBid + 1).getBidAmount() == clearingPrice;
		} catch (IndexOutOfBoundsException e) {
			return false;
		}
	}

	private boolean isPreviousBidPlacedAtClearingPrice(
			ArrayList<Bid> sortedBids, double clearingPrice, int indexOfBid) {
		try {
			return sortedBids.get(indexOfBid - 1).getBidAmount() == clearingPrice;
		} catch (IndexOutOfBoundsException e) {
			return false;
		}
	}

	private boolean hasMultipleBidsAtClearingPrice(ArrayList<Bid> sortedBids,
			double clearingPrice, int indexOfCutOffBid) {

		return isNextBidPlacedAtClearingPrice(sortedBids, clearingPrice,
				indexOfCutOffBid)
				|| isPreviousBidPlacedAtClearingPrice(sortedBids,
						clearingPrice, indexOfCutOffBid);
	}

	private void processBidsForSection(Section section,
			ArrayList<Bid> sortedBids, int roundNo) {

		if (sortedBids.isEmpty()) {
			return;
		}

		/*
		 * Compute vacancy
		 */
		int vacancy = sectionStudentCtrl.computeVacancyForSection(section);

		/*
		 * Compute index of last clearable bid
		 */
		int indexOfLastClearableBid = computeIndexOfLastClearableBid(
				sortedBids, vacancy, roundNo);

		/*
		 * Add successful bids to section student table
		 */
		ArrayList<Bid> succesfulBids = findSuccessfulBids(sortedBids,
				indexOfLastClearableBid);
		sectionStudentMgr.addMultipleSectionStudentEnrollments(succesfulBids);

		/*
		 * Refund e$ for unsuccessful bids
		 */
		ArrayList<Bid> unsuccessfulBids = findUnsuccessfulBids(sortedBids,
				indexOfLastClearableBid);
		studentMgr.refundEdollars(unsuccessfulBids);

	}

	/**
	 * Adds successful bids to section student table. Refunds e$ back to
	 * students for unsuccessful bids.
	 */
	protected void processBids(int roundNo) {

		/*
		 * Retrieve a list of sections that have bids
		 */
		ArrayList<Section> sectionsWithBids = bidMgr
				.retrieveAllSectionsWithBids();

		/*
		 * Iterate through each section and process bids
		 */
		for (Section eachSection : sectionsWithBids) {
			ArrayList<Bid> sortedBids = bidMgr
					.retrieveBidsForSectionOrderedByHighestBidAmount(
							eachSection.getCourse().getCourseCode(),
							eachSection.getSectionCode());
			processBidsForSection(eachSection, sortedBids, roundNo);
		}

	}

}
